<?php //strip
/**
 * Helper class for creating data tables for the WordPress admin.
 * 
 * @package odWpPlugin
 * @author Ondrej Donek <ondrejd@gmail.com>
 * 
 * @todo Better custom links!
 * @todo If is target model instance of odWpPlugin_Model_WithTrash or 
 *       odWpPlugin_Model_WithTrashAndVisibility automatically set 
 *       odWpPlugin_Admin_Table::$mUseDelete as TRUE and acordingly allow 
 *       the delete/undelete bulk actions, delete custom link per each item 
 *       as well as deleted filter.
 * @todo If user has filtered deleted items should be added custom link delete 
 *       permanently as well the same bulk action.
 */
class odWpPlugin_Admin_Table
{
  const TABLE_COLUMN_TYPE_BOOLEAN = 'boolean';
  const TABLE_COLUMN_TYPE_EMAIL = 'email';
  
  /** 
   * Use instance of odWpPlugin_Model_* or method self::getItems(...) instead of.
   * @var boolean
   */
  protected $mUseModel = false;
  
  /**
   * Hold database model attached to the table.
   * @var odWpPlugin_Model_Interface 
   */
  protected $mModel = null;
  
  /** 
   * URL of the table's form.
   * @var string 
   */
  protected $mTableFormUrl = '';
  
  /** 
   * URL of item's editing page.
   * @var string
   */
  protected $mTableEditUrl = '';
  
  /** 
   * Holds description of table's columns.
   * @var array 
   */
  protected $mColumns = array();
  
  /** 
   * Uses model `published` pattern?
   * @var boolean 
   */
  protected $mUsePublished = false;
  
  /** 
   * Uses model `deleted` pattern?
   * @var boolean 
   */
  protected $mUseDeleted = false;
  
  /** 
   * Are bulk actions enabled?
   * @var boolean
   */
  protected $mBulkActions = false;
  
  /** 
   * Bulk action messages.
   * @var array 
   */
  protected $mBulkActionsMessages = array(
      'noitems'   => 'You did not select any items!',
      'noaction'        => 'You did not select any action to perform!',
      'delete'          => 'Selected items were deleted.',
      'undelete'        => 'Selected items were undeleted.',
      'publish'         => 'Selected items were published.',
      'unpublish' => 'Selected items were unpublished.'
  );
  
  /** 
   * Holds name of the primary column.
   * @var string
   */
  protected $mPrimaryColumn = null;
  
  /** 
   * Contains id of column where should be edit link rendered. To use it the 
   * `$this->mRenderEditLink` must be TRUE. This means another edit link - 
   * the one is still rendered in the last column.
   * @var string
   */
  protected $mEditLinkColumn = null;

  /** 
   * Render edit link on each row?
   * @var unknown_type
   */
  protected $mRenderEditLink = false;

  /** 
   * Render delete link on each row?
   * @var boolean 
   */
  protected $mRenderDeleteLink = false;

  /** 
   * Holds custom links
   * @var array
   */
  protected $mCustomLinks = null;
  
  /**
   * Construct table.
   *
   * @param array $aOptions
   * @return void
   */
  public function __construct($aOptions = array())
  {
    // Default messages for bulk actions
    $this->mBulkActionsMessages['noitems'] = __('You did not select any items!', 'odWpPlugin');
    $this->mBulkActionsMessages['noaction'] = __('You did not select any action to perform!', 'odWpPlugin');
    $this->mBulkActionsMessages['delete'] = __('Selected items were deleted.', 'odWpPlugin');
    $this->mBulkActionsMessages['undelete'] = __('Selected items were undeleted.', 'odWpPlugin');
    $this->mBulkActionsMessages['publish'] = __('Selected items were published.', 'odWpPlugin');
    $this->mBulkActionsMessages['unpublish'] = __('Selected items were unpublished.', 'odWpPlugin');
    
    if(!is_array($aOptions)) {
      throw new Exception('Expected an array as a argument!');
    }
    
    // If `mUseModel` is FALSE these method should be defined:
    //    - getItemsCount(...)
    //    - getItems(...)
    $this->mUseModel = (!array_key_exists('use_model', $aOptions)) 
        ? false 
        : (bool) $aOptions['use_model'];
    
    if($this->mUseModel === true && !array_key_exists('model', $aOptions)) {
      throw new Exception('Unable to found `model` in passed options!');
    }
    
    if($this->mUseModel === true) {
      if(!($aOptions['model'] instanceof odWpPlugin_Model_Interface)) {
        throw new Exception('Option `model` expected to be implementation of `odWpPlugin_Model_Interface!');
      }
    }
    $this->mModel = $aOptions['model'];
    
    // Columns
    //if(!array_key_exists('columns', $aOptions)) {
    //  throw new Exception('Automatical creating table columns from database table columns is not implemented!');
    //}
    $this->mColumns = $aOptions['columns'];
    
    // Primary column (holds index in the model, default is `ID`).
    $this->mPrimaryColumn = (array_key_exists('primaryCol', $aOptions))
        ? $aOptions['primaryCol']
        : null;
    if(is_null($this->mPrimaryColumn) && $this->mUseModel === true){
      $this->mPrimaryColumn = ($this->mModel->hasColumn('ID')) ? 'ID' : null;
    }
    if(is_null($this->mPrimaryColumn)) {
      throw new Exception('You must define a primary column!');
    }
    
    // Form URL
    $this->mTableFormUrl = (array_key_exists('formUrl', $aOptions))
        ? $aOptions['formUrl']
        : null;
    if(is_null($this->mTableFormUrl)) {
      throw new Exception('You must define a URL of table\'s form!');
    }
    
    // Edit URL
    $this->mTableEditUrl = (array_key_exists('editUrl', $aOptions))
        ? $aOptions['editUrl']
        : null;
    if(is_null($this->mTableEditUrl)) {
      $this->mTableEditUrl = $this->mTableFormUrl;
    }
    
    // Check if model uses `published` pattern
    if($this->mUseModel === true) {
      if($this->mModel->hasColumn('published')) {
        $this->mUsePublished = true;
      }
    }
    
    // Check if model uses `deleted` pattern
    if($this->mUseModel === true) {
      if($this->mModel->hasColumn('deleted')) {
        $this->mUseDeleted = true;
      }
    }
    
    // Bulk actions
    if(array_key_exists('bulkActions', $aOptions)) {
      if($aOptions['bulkActions'] === true) {
        $this->mBulkActions = true;
        
        if(array_key_exists('bulkActionsMsgs', $aOptions)) {
          $this->mBulkActionsMessages = $aOptions['bulkActionsMsgs'];
        }
      }
    }
    // TODO $this->_resolveBulkActions()
    
    // Filters
    // ...............
    
    $this->mRenderEditLink = (array_key_exists('editLink', $aOptions))
        ? (($aOptions['editLink'] === true) ? true : false)
        : true; // default value
    
    $this->mRenderDeleteLink = (array_key_exists('deleteLink', $aOptions))
        ? (($aOptions['deleteLink'] === true) ? true : false)
        : true; // default value
    
    $this->mCustomLinks = (array_key_exists('customLinks', $aOptions)) 
        ? $aOptions['customLinks'] 
        : array();
  }
  
  /**
   * Resolve all bulk actions (using assigned database model).
   *
   * @return void
   */
  protected function resolveBulkActions()
  {
    if((isset($_POST['doaction_1']) || isset($_POST['doaction_2']))) {
      $action = ($_POST['action_1'] != '-1') ? $_POST['action_1'] : $_POST['action_2'];
      $items = (isset($_POST['items'])) ? $_POST['items'] : array();
      
      if(count($items) == 0) {
        $this->mRenderer->adminMsg($this->mBulkActionsMessages['noitems'], 'error');
      } else {
        switch($action) {
          case ('delete' && $this->mUseDeleted === true):
            $this->mModel->delete($items);
            $this->mRenderer->adminMsg($this->mBulkActionsMessages['delete']);
            break;
          
          case ('undelete' && $this->mUseDeleted === true):
            $this->mModel->undelete($items);
            $this->mRenderer->adminMsg($this->mBulkActionsMessages['undelete']);
            break;
          
          case ('publish' && $this->mUsePublished === true):
            $this->mModel->publish($items);
            $this->mRenderer->adminMsg($this->mBulkActionsMessages['delete']);
            break;
          
          case ('unpublish' && $this->mUsePublished === true):
            $this->mModel->unpublish($items);
            $this->mRenderer->adminMsg($this->mBulkActionsMessages['undelete']);
            break;
          
          case '-1':
          default:
            $this->mRenderer->adminMsg(__('You didn\'t select action you want to perform!', 'odWpPlugin'), 'error');
            break;
        }
      }
    }
  }
  
  /**
   * Renders table navigation.
   * 
   * @param integer $aIndex 
   * @param integer $aItemsCount
   * @param integer $aPerPage
   * @param integer $aPagesCount
   * @param integer $aLimitFrom
   * @return void
   * @todo Finish actions!
   * @todo Finish pages counter!
   */
  private function _renderTableNavigation($aIndex, $aItemsCount, $aPerPage, $aPagesCount, $aPage, $aLimitFrom)
  {
    $output = '';
    $output .= '<div class="tablenav">' . PHP_EOL;
    
    if($this->mBulkActions === true) { 
      $output .= '<div class="alignleft actions">' . PHP_EOL;
      $output .= '<select name="action_' . $aIndex . '" class="select-action">' . PHP_EOL;
      $output .= '<option value="-1" selected="selected">' . __('Bulk actions', 'odWpPlugin') . '</option>' . PHP_EOL;
      $output .= '<option value="delete">' . __('Delete', 'odWpPlugin') . '</option>' . PHP_EOL;
      $output .= '<option value="undelete">' . __('Undelete', 'odWpPlugin') . '</option>' . PHP_EOL;
      $output .= '</select>' . PHP_EOL;
      $output .= '<input type="submit" value="' . __('Apply', 'odWpPlugin') . '" name="doaction_' . $aIndex . '" id="doaction_' . $aIndex . '" class="button-secondary action" />'. PHP_EOL;
      $output .= '</div>' . PHP_EOL;
    }
    
    if($aPagesCount > 0) {
      $output .= '<div class="tablenav-pages">';
      $output .= '<span class="displaying-num">' . __('Showing ', 'odWpPlugin') .
          $aLimitFrom . '-' . 0 .
          __(' from ', 'odWpPlugin') . $aItemsCount . '.</span> ';
      
      $from = $aPage;
      $to = $aPagesCount;
      
      if($aPagesCount > 5) {
        if($aPage > 5) {
          $from = $aPage - 2;
          $to = $aPage + 2;
        } else {
          $from = 0;
          $to = 5;
        }
      }
      
      if($aPage > 0) {
        $output .= '<a href="' . $this->mTableFormUrl . '&amp;tpp=' . 
            $aPerPage . '&amp;tp=' . ($aPage - 1) . 
            '" class="previous page-numbers">«</a>'; 
      }
      
      for($i=$from; $i<$to; $i++) {
        if($i == $aPage) {
          $output .= '<span class="page-numbers current">' . ($i + 1) . '</span>&nbsp;';
        } else {
          $output .= '<a href="' . $this->mTableFormUrl . '&amp;tpp=' . $aPerPage . 
              '&amp;tp=' . $i . '" class="page-numbers">' . ($i + 1) . '</a>';
        }
      }
      
      if($aPagesCount > 5) {
        $output .= '<a href="' . $this->mTableFormUrl . '&amp;tpp=' . $aPerPage . 
            '&amp;tp=' . ($aPage + 1) . '" class="next page-numbers">»</a>'; 
      }
      
      $output .= '</div>';
    }
    
    $output .= '</div>';
    $output .= '<br class="clear" />' . PHP_EOL;
    
    return $output;
  }
  
  /**
   * Renders table header columns.
   * 
   * @return string
   */
  private function _renderTableColumns()
  {
    $output = '';
    
    foreach($this->mColumns as $colKey => $colOpts) {
      $primary = false;
      $style = '';
      
      if(array_key_exists('hidden', $colOpts))
        if($colOpts['hidden'] === true)
          continue;
      
      if(array_key_exists('primary', $colOpts)) 
        if($colOpts['primary'] === true) 
          $primary = true;
      
      if(array_key_exists('style', $colOpts))
        $style = $colOpts['style'];
      
      $output .= '<th scope="col" style="' . ((!empty($style)) ? $style : '') . '" class="manage-column column-cb';
      
      // primary column is with the checkbox (we are expecting that it's hold ID)
      if($primary === true) {
        $output .= ' check-column';
      }
      
      $output .= '">';
      
      if($primary === true) {
        $output .= '<input type="checkbox"/>';
      } else {
        $output .= $colOpts['label'];
      }
      
      $output .= '</th>';
    }
    
    return $output;
  }

  /**
   * Returns rendered column value.
   * 
   * @param string $aValue
   * @param string $aType
   * @return string
   */
  private function _renderColumnValue($aValue, $aType = 'common')
  {
    $ret = '';
    switch($aType) {
      case 'boolean':
        $ret = ($aValue == 1 || $aValue == '1')
            ? '<span style="color: green;"> ' . __('Yes', 'odWpPlugin') . '</span>'
            : '<span style="color: #f30;;"> ' . __('No', 'odWpPlugin') . '</span>';
        break;

      case 'email':
        $ret = '<a href="mailto:' . $aValue . '">' . $aValue . '</a>';
        break;

      case 'common':
      default:
        $ret .= $aValue;
        break;
    }

    return $ret;
  }

  /**
   * Render single table row.
   * 
   * @param array $aValues Values for the table row.
   * @param integer $aIndex
   * @return string
   */
  private function _renderTableRow($aValues, $aIndex = 0)
  {
    $output = '';
    $output .= '<tr class="' . (($aIndex++%2) ? 'alternate' : '' ) . ' status-inherit">' . PHP_EOL;
    
    foreach($this->mColumns as $colKey => $colOpts) {
      $primary = false;
      $control = false;
      
      if(array_key_exists('hidden', $colOpts))
        if($colOpts['hidden'] === true)
          continue;
      
      if(array_key_exists('primary', $colOpts)) 
        if($colOpts['primary'] === true) 
          $primary = true;
      
      if(array_key_exists('control', $colOpts))
        if($colOpts['control'] === true)
          $control = true;
      
      $id = $aValues[$this->mPrimaryColumn];
      $editUrl = $this->mTableEditUrl . '&amp;item_ID=' . $id;
      $type = (array_key_exists('type', $colOpts)) 
          ? $colOpts['type'] 
          : 'common';
      $value = '';
      
      if(array_key_exists('valueCol', $colOpts)) {
        if(is_array($colOpts['valueCol'])) {
          $sep = array_key_exists('valueSep', $colOpts) 
              ? $colOpts['valueSep']
              : ' | ';
          foreach($colOpts['valueCol'] as $valueCol) {
            $value .= ((empty($value)) ? '' : $sep) . $this->_renderColumnValue($aValues[$valueCol], $type);
          }
        } else {
          $value = $this->_renderColumnValue($aValues[$colOpts['valueCol']], $type);
        }
      } else {
        $value = $this->_renderColumnValue($aValues[$colKey], $type);
      }
      
      if($primary === true) {
        $output .= '<th scope="row" class="check-column"><input type="checkbox" ' .
            'name="items[]" value="' . $id . '"/></th>' . PHP_EOL;
      }
      else if($control === true) {
        $output .= '<td class="column-title">' .
            '<strong><a href="' . $editUrl . '" class="row-title">' .
            $value .
            '</a></strong>';
        
        if($this->mRenderEditLink === true || $this->mRenderDeleteLink === true) {
          $output .= '<div class="row-actions">';
          $sep = '';
          
          if($this->mRenderEditLink === true) {
            $output .= '<span class="edit"><a href="' . $editUrl . '">' . __('Edit', 'odWpPlugin') . '</a></span>';
            $sep = ' | ';
          }
          
          $output .= $sep;
          $sep = '';
          
          if($this->mRenderDeleteLink === true) {
            $output .= '<span class="trash"><a href="' . $this->mTableFormUrl . 
                '&amp;delete=' . $id . '">' . __('Delete', 'odWpPlugin') . '</a></span>';
            $sep = ' | ';
          }

          $output .= $sep;
          $clc = count($this->mCustomLinks);

          if($clc > 0) {
            for($i = 0; $i < $clc; $i++) {
              $link = $this->mCustomLinks[$i];
              $output .= '<span class="edit"><a href="' . str_replace('%ITEM_ID%', $id, $link['link']) . '" ' . 
                  ((array_key_exists('description', $link)) ? ' title="' . $link['description'] . '"' : '') . 
                  '>' . $link['label'] . '</a></span>';
              $output .= ((($i+1) < $clc) ? ' | ' : '');
            }
          }
          
          $output .= '</div>';
        }
        
        $output .= '</td>' . PHP_EOL;
      }
      else {
        $output .= '<td>' . $value . '</td>' . PHP_EOL;
      }
    }
    
    $output .= '</tr>' . PHP_EOL;
    
    return $output;
  }
  
  /**
   * Renders table into the string and returns it.
   *
   * @return string
   */
  public function render()
  {
    $itemsCount = ($this->mUseModel === true)
        ? (($this->mUseDeleted === true) 
            ? $this->mModel->getCount('`t1`.`deleted` = 0 ') 
            : $this->mModel->getCount('1 '))
        : (($this->mUseDeleted === true) 
            ? $this->getItemsCount('`t1`.`deleted` = 0 ')
            : $this->getItemsCount('1 '));
    $perPage = (isset($_GET['tpp'])) ? (int) $_GET['tpp'] : 10;
    $pagesCount = ceil($itemsCount / $perPage);
    $page = (isset($_GET['tp'])) ? (int) $_GET['tp'] : 0;
    $limitFrom = ($page == 0) ? 0 : $page * $perPage;
    $items = ($this->mUseModel === true)
        ? (($this->mUseDeleted === true) 
            ? $this->mModel->select('`t1`.`deleted` = 0 ', $perPage, $limitFrom)
            : $this->mModel->select('1 ', $perPage, $limitFrom))
        : (($this->mUseDeleted === true) 
            ? $this->getItems('`t1`.`deleted` = 0 ', $perPage, $limitFrom)
            : $this->getItems('1 ', $perPage, $limitFrom));
    
    $output = '';
    $output .= $this->_renderTableNavigation('1', $itemsCount, $perPage, $pagesCount, $page, $limitFrom);
    $output .= '<table class="widefat post fixed" cellspacing="0">' . PHP_EOL;
    $output .= '<thead><tr>' . $this->_renderTableColumns() . '</tr></thead>' . PHP_EOL;
    $output .= '<tfoot><tr>' . $this->_renderTableColumns() . '</tr></tfoot>' . PHP_EOL;
    $output .= '<tbody id="the-list" class="list:odWpPlugin-list">' . PHP_EOL;
    
    if(!is_null($items)) {
      $row = 0;
      foreach($items as $item) {
        $output .= $this->_renderTableRow($item, $row);
        $row++;
      }
    }
    
    $output .= '</tbody>' . PHP_EOL;
    $output .= '</table>' . PHP_EOL;
    $output .= $this->_renderTableNavigation('1', $itemsCount, $perPage,
                                                                                     $pagesCount, $page, $limitFrom);
    $output .= '</form>' . PHP_EOL;
    
    return $output;
  }
  
  /**
   * Set specified column hidden.
   * 
   * @param string $aColumn
   * @return boolean
   * @throws Exception whenever the specified column was not found.
   */
  public function hideColumn($aColumn)
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    $this->mColumns[$aColumn]['hidden'] = true;
  }
  
  /**
   * Set specified column shown.
   * 
   * @param string $aColumn
   * @return boolean
   * @throws Exception whenever the specified column was not found.
   */
  public function showColumn($aColumn)
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    if(array_key_exists('hidden', $this->mColumns[$aColumn])) {
      unset($this->mColumns[$aColumn]['hidden']);
    }
  }
  
  /**
   * Returns TRUE if specified column is hidden.
   * 
   * @param string $aColumn
   * @return boolean
   * @throws Exception whenever the specified column was not found.
   */
  public function isColumnHidden($aColumn)
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    if(!array_key_exists('hidden', $this->mColumns[$aColumn])) {
      return false;
    }
    
    return $this->mColumns[$aColumn]['hidden'];
  }
  
  /**
   * Get property's value of specified column. If column hasn't value 
   * for requested property set yet returns NULL.
   * 
   * @param string $aColumn Name of column.
   * @param string $aProperty Name of requested property.
   * @param mixed $aDefault Default value (optional).
   * @return mixed
   * @throws Exception whenever the specified column was not found.
   */
  public function getColumnProperty($aColumn, $aProperty, $aDefault = null) 
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    if(!array_key_exists($aProperty, $this->mColumns[$aColumn])) {
      return $aDefault;
    }
    
    return $this->mColumns[$aColumn][$aProperty];
  }
  
  /**
   * Set property's value of specified column.
   * 
   * @param string $aColumn Name of column.
   * @param string $aProperty Name of requested property.
   * @param mixed $aValue New value for the property.
   * @return boolean
   * @throws Exception whenever the specified column was not found.
   */
  public function setColumnProperty($aColumn, $aProperty, $aValue) 
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    $this->mColumns[$aColumn][$aProperty] = $aValue;
    
    return true;
  }
  
  /**
   * Get all properties of specified column.
   * 
   * @param string $aColumn Name of column.
   * @return array
   * @throws Exception whenever the specified column was not found.
   * @todo Implement this!
   */
  public function getColumnProperties($aColumn) 
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    throw new Exception('odWpPlugin_Admin_Table::getColumnProperties(*) is not implemented yet!');
  }
  
  /**
   * Set properties of specified column.
   * 
   * @param string $aColumn Name of column.
   * @param array $aProperties
   * @return mixed
   * @throws Exception whenever the specified column was not found.
   * @todo Implement this!
   */
  public function setColumnProperties($aColumn, $aProperties = array()) 
  {
    if(!array_key_exists($aColumn, $this->mColumns)) {
      throw new Exception('Specified column [' . $aColumn . '] was not found!');
    }
    
    throw new Exception('odWpPlugin_Admin_Table::setColumnProperties(*) is not implemetned yet!');
  }
  
} // End of odWpPlugin_Admin_Table
